use spring_axum::{
    controller, route_get, route_post, route_put, AxumBody, AxumRequest, Json, Path, Router,
    StatusCode, Interceptor, InterceptorLayer, Component, ApplicationContext,
};
use tower::ServiceExt;
use spring_axum::Controller;
use axum::body::to_bytes;
use serde::Deserialize;
use serde_json::json;
use std::sync::Arc;

#[route_get("/t/hello")]
async fn t_hello() -> &'static str { "ok" }

#[route_post("/t/echo")]
async fn t_echo(Json(v): Json<serde_json::Value>) -> String { v["msg"].as_str().unwrap_or("").to_string() }

#[derive(Deserialize)]
struct User { name: String }

#[route_put("/t/users/{id}")]
async fn t_update(Path(id): Path<String>, Json(user): Json<User>) -> String { format!("{}:{}", id, user.name) }

#[controller(t_hello, t_echo, t_update)]
struct TestController;

#[tokio::test]
async fn macros_routes_work() {
    let router = Router::new().merge(TestController.routes());

    // GET
    let req = AxumRequest::builder().uri("/t/hello").body(AxumBody::empty()).unwrap();
    let res = router.clone().oneshot(req).await.unwrap();
    assert_eq!(res.status(), StatusCode::OK);

    // POST JSON echo
    let req = AxumRequest::builder()
        .method("POST")
        .uri("/t/echo")
        .header("content-type", "application/json")
        .body(AxumBody::from(serde_json::to_string(&json!({"msg":"hi"})).unwrap()))
        .unwrap();
    let res = router.clone().oneshot(req).await.unwrap();
    let bytes = to_bytes(res.into_body(), usize::MAX).await.unwrap();
    assert_eq!(std::str::from_utf8(&bytes).unwrap(), "hi");

    // PUT with path + json
    let req = AxumRequest::builder()
        .method("PUT")
        .uri("/t/users/42")
        .header("content-type", "application/json")
        .body(AxumBody::from(serde_json::to_string(&json!({"name":"neo"})).unwrap()))
        .unwrap();
    let res = router.clone().oneshot(req).await.unwrap();
    let bytes = to_bytes(res.into_body(), usize::MAX).await.unwrap();
    assert_eq!(std::str::from_utf8(&bytes).unwrap(), "42:neo");
}

#[derive(Clone)]
struct GreetingService;
impl GreetingService { fn greet(&self, name: &str) -> String { format!("Hello, {}", name) } }

#[tokio::test]
async fn component_extractor_resolves_from_extensions() {
    let ctx = ApplicationContext::new();
    ctx.register(GreetingService);
    let ctx_arc = Arc::new(ctx);

    let mut req = AxumRequest::new(AxumBody::empty());
    req.extensions_mut().insert(ctx_arc.clone());
    let (mut parts, _) = req.into_parts();

    let comp = <Component<GreetingService> as axum::extract::FromRequestParts<()>>::from_request_parts(&mut parts, &()).await.unwrap();
    assert_eq!(comp.0.greet("Axum"), "Hello, Axum");
}

#[derive(Clone)]
struct TestInterceptor;
impl Interceptor for TestInterceptor {
    fn on_response(&self, mut res: spring_axum::AxumResponse<spring_axum::AxumBody>) -> spring_axum::AxumResponse<spring_axum::AxumBody> {
        res.headers_mut().insert(
            axum::http::header::HeaderName::from_static("x-test"),
            axum::http::HeaderValue::from_static("yes"),
        );
        res
    }
}

#[tokio::test]
async fn interceptor_adds_response_header() {
    let router = Router::new()
        .route("/t/intercept", spring_axum::get(|| async { "ok" }))
        .layer(InterceptorLayer::new(TestInterceptor));

    let req = AxumRequest::builder().uri("/t/intercept").body(AxumBody::empty()).unwrap();
    let res = router.oneshot(req).await.unwrap();
    assert_eq!(res.headers().get("x-test").unwrap(), "yes");
}